package com.lt.flashcard.model.service;


import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.bson.types.ObjectId;
import org.hibernate.Criteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.zkoss.zk.ui.Executions;

import com.google.code.morphia.query.Query;
import com.lt.flashcard.TextUtil;
import com.lt.flashcard.Utility;
import com.lt.flashcard.model.entity.ActionResult;
import com.lt.flashcard.model.entity.AddCoinHistory;
import com.lt.flashcard.model.entity.Cart;
import com.lt.flashcard.model.entity.CartItem;
import com.lt.flashcard.model.entity.FlashcardSet;
import com.lt.flashcard.model.entity.Medal;
import com.lt.flashcard.model.entity.Orderdetail;
import com.lt.flashcard.model.entity.Orderproduct;
import com.lt.flashcard.model.entity.Partner;
import com.lt.flashcard.model.entity.PaymentResult;
import com.lt.flashcard.model.entity.PhoneCardInfo;
import com.lt.flashcard.model.entity.PhoneCardResponse;
import com.lt.flashcard.model.entity.ProductCode;
import com.lt.flashcard.model.entity.ProductCodeHistory;
import com.lt.flashcard.model.entity.Productcategory;
import com.lt.flashcard.model.entity.Promotion;
import com.lt.flashcard.model.entity.User;

/**
 * The service that take care about ecommerce of system
 * @author ocsen
 *
 */
public class EcommerceService {
	
	private static final Logger log = Logger.getLogger(EcommerceService.class);

	private BasicDAO<Productcategory> _productcategoryDAO = null;
	/**
	 * Get DAO to interact with ProductCategory in database
	 * @return
	 */
	private BasicDAO<Productcategory> getProductCategoryDAO() {
		if(_productcategoryDAO == null)
			_productcategoryDAO = new BasicDAO<Productcategory>(Productcategory.class);
		return _productcategoryDAO;
	}
	
	private BasicDAO<Orderdetail> _orderdetailDAO = null;
	/**
	 * Get DAO to interact with Orderdetail
	 * @return
	 */
	private BasicDAO<Orderdetail> getOrderDetailDAO() {
		if(_orderdetailDAO == null)
			_orderdetailDAO = new BasicDAO<Orderdetail>(Orderdetail.class);
		return _orderdetailDAO;
	}
	
	private com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId> _productcodeDAO = null;
	/**
	 * Get DAO to interact with productcode stored in database
	 * @return
	 */
	private com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId> getProductCodeDAO() {
		if(_productcodeDAO == null) {
			_productcodeDAO = new com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId>(ProductCode.class, Repository.getInstance().getDatastore());
		}
		return _productcodeDAO;
	}
	
	private com.google.code.morphia.dao.BasicDAO<Partner, ObjectId> _partnerDAO = null;
	/**
	 * Get DAO to interact with partner stored in database
	 * @return
	 */
	private com.google.code.morphia.dao.BasicDAO<Partner, ObjectId> getPartnerDAO() {
		if(_partnerDAO == null) {
			_partnerDAO = new com.google.code.morphia.dao.BasicDAO<Partner, ObjectId>(Partner.class, Repository.getInstance().getDatastore());
		}
		return _partnerDAO;
	}
	
	private com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> _promotionDAO = null;
	/**
	 * Get DAO to interact with productcode stored in database
	 * @return
	 */
	private com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> getPromotionDAO() {
		if(_promotionDAO == null) {
			_promotionDAO = new com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId>(Promotion.class, Repository.getInstance().getDatastore());
		}
		return _promotionDAO;
	}
	
	private BasicDAO<Orderproduct> _orderproductDAO = null;
	private BasicDAO<Orderproduct> getOrderproductDAO() {
		if(_orderproductDAO == null)
			_orderproductDAO = new BasicDAO<Orderproduct>(Orderproduct.class);
		return _orderproductDAO;
	}
	
	private com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId> _addcoinDAO = null;
	/**
	 * Get DAO to interact with productcode stored in database
	 * @return
	 */
	private com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId> getAddCoinDAO() {
		if(_addcoinDAO == null) {
			_addcoinDAO = new com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId>(AddCoinHistory.class, Repository.getInstance().getDatastore());
		}
		return _addcoinDAO;
	}
	
	/**
	 * Return current cart object for current session
	 * Allow cart for anonymous user
	 * @return
	 */
	public Cart getCurrentCart() {
		AAService aasrv = Repository.getInstance().getAAService();
		Session ses = aasrv.getCurrentSubject().getSession();
		Cart cart =  (Cart) ses.getAttribute("cart");
		if(cart == null) {
			cart = new Cart();
			ses.setAttribute("cart", cart);
		}
		return cart;
	}
	
	/**
	 * Get list of purchased products
	 * @param user
	 * @return
	 */
	public Set<Long> getPurchasedProductsId(User user) {
		//TODO optimize this use cache
		org.hibernate.Session ses = Repository.getInstance().getCurrentSession();
		org.hibernate.Query query = ses.createQuery("select originalflashcardsetCache from FlashcardSet where admin=2 and author=:user");
		query.setParameter("user", user);
		Iterator it = query.iterate();
		Set<Long> ret = new HashSet<Long>();
        while (it.hasNext()) {
            try {
                ret.add(new Long(String.valueOf(it.next())));
            } catch (Exception e) {
            }
        }
		return ret;
	}
	
	/**
	 * Destroy cart data, for logout or when finish shopping
	 */
	public void destroyCart() {
		Subject subject = Repository.getInstance().getAAService().getCurrentSubject();
		if(subject != null) {
			Session ses = subject.getSession();
			ses.removeAttribute("cart");
		}
	}
	
	/**
	 * Get list of product categories
	 * @return
	 */
	public List<Productcategory> getParentProductCategories() {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		Criteria crit = dao.createCriteria();
		crit.add(Restrictions.isNull("parent"));
		crit.add(Restrictions.eq("display", 1));
		crit.addOrder(Order.asc("weight"));
		crit.addOrder(Order.asc("name"));
		return crit.list();
	}
	
	/**
	 * Get all products that belong to a category
	 * @param cateid
	 * @return
	 */
	public List<FlashcardSet> getProductsOfCategory(Long cateid) {
		Criteria crit = Repository.getInstance().getFlashcardSetService().createCriteria();
		crit.add(Restrictions.eq("readysale", (byte) 1));
		crit.createAlias("productcategory", "cat");
		crit.add(Restrictions.eq("cat.id", cateid));
		crit.addOrder(Order.desc("weight"));
		return crit.list();
	}
	
	/**
	 * Get list of product by conditions
	 * @param name
	 * @param category
	 * @param isEFlashcard
	 * @param isPaper
	 * @param pagesize
	 * @param page
	 * @return
	 */
	public List<FlashcardSet> getProductByFilter(String name, Long category, boolean isEFlashcard, boolean isPaper, int pagesize, int page) {
		FlashcardSetService srv = Repository.getInstance().getFlashcardSetService();
		Criteria crit = srv.createCriteria();
		crit.add(Restrictions.eq("readysale", (byte) 1));
		crit.add(Restrictions.eq("admin", FlashcardSet.ADMIN_YES));
        if (StringUtils.isNotBlank(name))
			crit.add(Restrictions.like("name", name, MatchMode.ANYWHERE));
		if(category != 0) {
			crit.createAlias("productcategory", "cat");
			crit.add(Restrictions.eq("cat.id", category));
		}
		if(isPaper)
			crit.add(Restrictions.eq("haspaper", (byte) 1));
		if(pagesize > 0) {
			int from = pagesize * page;
			crit.setFirstResult(from);
			crit.setMaxResults(pagesize);
		}
		crit.addOrder(Order.desc("weight"));
		crit.addOrder(Order.asc("name"));
		return crit.list();
	}
	
	/**
	 * Count the number of product by filter
	 * @param name
	 * @param category
	 * @param isEFlashcard
	 * @param isPaper
	 * @return
	 */
	public int countProductByFilter(String name, Long category, boolean isEFlashcard, boolean isPaper) {
		FlashcardSetService srv = Repository.getInstance().getFlashcardSetService();
		Criteria crit = srv.createCriteria();
		crit.add(Restrictions.eq("readysale", (byte) 1));
		crit.add(Restrictions.eq("admin", FlashcardSet.ADMIN_YES));
        if (StringUtils.isNotBlank(name))
			crit.add(Restrictions.like("name", name, MatchMode.ANYWHERE));
		if(category != 0) {
			crit.createAlias("productcategory", "cat");
			crit.add(Restrictions.eq("cat.id", category));
		}
		if(isPaper)
			crit.add(Restrictions.eq("haspaper", (byte) 1));
		crit.setProjection(Projections.rowCount());
		return (Integer) crit.uniqueResult();
	}
	
	/**
	 * Convert money to coin
	 * @param money
	 * @return
	 */
	public int priceToCoin(int money) {
		return money / 1000;
	}
	
	/**
	 * Convert money to coin with bonus
	 * @param money
	 * @return
	 */
	public int priceToCoinWithBonus(int money) {
		int amount = money / 1000;
		if(amount < 30)
			amount += 0;
		else
			amount = (int) Math.ceil(amount + amount * 0.1);
		return amount;
	}
	
	/**
	 * Transfer coin to money
	 * @param coin
	 * @return
	 */
	public int coinToPrice(int coin) {
		return coin * 1000;
	}
	
	/**
	 * Get order history of user
	 * @param user
	 * @return
	 */
	public List<Orderdetail> getOrderHistory(User user) {
		BasicDAO<Orderdetail> srv = getOrderDetailDAO();
		Criteria crit = srv.createCriteria();
		crit.addOrder(Order.desc("created"));
		crit.add(Restrictions.eq("user", user));
		return crit.list();
	}
	
	/**
	 * Find an order by id
	 * @param id
	 * @return
	 */
	public Orderdetail getOrderdetail(Long id) {
		BasicDAO<Orderdetail> srv = getOrderDetailDAO();
		return srv.findByID(id);
	}
	
	/**
	 * Find order by code
	 * @param code
	 * @return
	 */
	public Orderdetail getOrderdetail(String code) {
		BasicDAO<Orderdetail> srv = getOrderDetailDAO();
		Criteria crit = srv.createCriteria();
		crit.add(Restrictions.eq("code", code));
		return (Orderdetail) crit.uniqueResult();
	}
	
	/**
	 * Save an order to database
	 * @param order
	 */
	public void saveOrderdetail(Orderdetail order) {
		getOrderDetailDAO().save(order);
	}
	
	/**
	 * Save a product code to database
	 * @param productcode
	 */
	public void saveProductCode(ProductCode productcode) {
		getProductCodeDAO().save(productcode);
	}
	
	/**
	 * Generate payment link for an order
	 * @param cart
	 * @return
	 */
	public String generatePaymentUrl(Orderdetail order) {
		PaymentService psrv = Repository.getInstance().getPaymentService();
		String order_description = "";
		String url = psrv.generatePaymentUrl("Payment for order: " + order.getId(), 
				order.getCode(),
				order.getPricetotal(),
				order.getOrderproducts().size(),
				0,
				0,
				order_description,
				order.getUser().getUsername(),
				order.getUser().getEmail(),
				"",
				"");
		if(log.isInfoEnabled())
			log.info(String.format("Payment url for order %s: %s", order.getId(), url));

		return url;
	}
	
	/**
	 * Generate payment url for add coin
	 * @param history
	 * @return
	 */
	public String generatePaymentUrl(AddCoinHistory history) {
		PaymentService psrv = Repository.getInstance().getPaymentService();
		UserService usrv = Repository.getInstance().getUserService();
		User user = usrv.findByID(history.getUserid());
		String order_description = String.format("Add coin to account %s", user.getEmail());
		
		String url = psrv.generatePaymentUrl("Add more coin", 
				"ADD:" + history.getId().toString(),
				history.getAmount(),
				1,
				0,
				0,
				order_description,
				user.getUsername(),
				user.getEmail(),
				"",
				"");
		
		if(log.isInfoEnabled())
			log.info(String.format("Payment url for add coin %s: %s", history.getId(), url));
		
		return url;
	}
	
	/**
	 * Apply list of promotions to user
	 * @param user
	 * @param promotions
	 * @param intransaction If not intransaction, will save the user
	 */
	public void applyPromotions(User user, List<Promotion> promotions, boolean intransaction) {
		LearningService nsv = Repository.getInstance().getLearningService();
		for(Promotion pro : promotions) {
			//only get one medal for each type
			switch(pro.getType()) {
			case Promotion.TYPE_ADDBLANKCARD:
				user.setMaxblankflashcard(user.getMaxblankflashcard() + pro.getValue());
				user.setCurrentblankflashcard(user.getCurrentblankflashcard() + pro.getValue());
				break;
			case Promotion.TYPE_ADDYELLOWCOIN:
				user.setYellowCoin(user.getYellowCoin() + pro.getValue());
				break;
			case Promotion.TYPE_ADDBLUECOIN:
				user.setBlueCoin(user.getBlueCoin() + pro.getValue());
				user.setTotalBlueCoin(user.getTotalBlueCoin() + pro.getValue());
				break;
			case Promotion.TYPE_GAINMEDAL:
				Medal medal = nsv.getMedal(pro.getMedalid());
				nsv.userGetMedal(user, medal);
				break;
			}
		}
		
		if(!intransaction) {
			UserService usrv = Repository.getInstance().getUserService();
			usrv.save(user);
		}
	}
	
	/**
	 * Apply promotions
	 * @param user
	 * @param targettype
	 * @param conditiontype
	 * @param data
	 * @param intransaction
	 */
	public void applyPromotions(User user, Integer targettype, Integer conditiontype, Object data, boolean intransaction) {
		List<Promotion> pro = getAllValidPromotions(targettype, conditiontype, data);
		if(pro != null && pro.size() > 0) {
			applyPromotions(user, pro, intransaction);
		}
	}
	
	/**
	 * Get all promotions in system
	 * @return
	 */
	public List<Promotion> getAllPromotions() {
		com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> dao = getPromotionDAO();
		Query<Promotion> query = dao.createQuery();
		query.order("created");
		return query.asList();
	}
	
	/**
	 * Get all valid promotion in system
	 * @return
	 */
	public List<Promotion> getAllValidPromotions() {
		com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> dao = getPromotionDAO();
		Query<Promotion> query = dao.createQuery();
		query.field("status").equal(Promotion.STATUS_VALID);
		query.or(query.criteria("validFrom").equal(null), 
				query.criteria("validFrom").lessThanOrEq(new Date()));
		query.or(query.criteria("validTo").equal(null), 
				query.criteria("validTo").greaterThanOrEq(new Date()));
		query.order("-created");
		return query.asList();
	}
	
	/**
	 * Get all valid promotion with condition type and extra information
	 * @param conditiontype
	 * @param data
	 * @return
	 */
	public List<Promotion> getAllValidPromotions(Integer targettype, Integer conditiontype, Object data) {
		com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> dao = getPromotionDAO();
		Query<Promotion> query = dao.createQuery();
		query.field("status").equal(Promotion.STATUS_VALID);
		query.or(query.criteria("validFrom").equal(null), 
				query.criteria("validFrom").lessThanOrEq(new Date()));
		query.or(query.criteria("validTo").equal(null), 
				query.criteria("validTo").greaterThanOrEq(new Date()));
		if(targettype != null)
			query.field("targetType").equal((int) targettype);
		if(conditiontype != null)
			query.field("conditiontype").equal((int) conditiontype);
		if(data != null && conditiontype != null) {
			switch(conditiontype) {
			case Promotion.CONDITIONTYPE_BUYITEM:
				query.field("targetId").equal(data);
				break;
			case Promotion.CONDITIONTYPE_ACTIVECODE:
				query.field("targetIdStr").equal(data);
				break;
			case Promotion.CONDITIONTYPE_FIRSTLOGIN:
				//No need extra information
				break;
			case Promotion.CONDITIONTYPE_KNOWWORDS:
			case Promotion.CONDITIONTYPE_LEARNED:
			case Promotion.CONDITIONTYPE_MAKEFRIEND:
			case Promotion.CONDITIONTYPE_KNOWWORDS_L0:
			case Promotion.CONDITIONTYPE_KNOWWORDS_L1:
			case Promotion.CONDITIONTYPE_KNOWWORDS_L7:
			case Promotion.CONDITIONTYPE_TESTCONTINUOUS:
				query.field("condition_value").lessThanOrEq(data);
				break;
			case Promotion.CONDITIONTYPE_LOGINAT:
				Date r = (Date) data;
				Calendar c = Calendar.getInstance();
				c.setTime(r);
				c.add(Calendar.DATE, -1);
				query.field("condition_date").lessThanOrEq(r);
				query.field("condition_date").greaterThanOrEq(c.getTime());
				break;
			case Promotion.CONDITIONTYPE_VISIT:
				query.field("condition_url").equal(data);
				break;
			}
		}
		query.order("-created");
		return query.asList();
	}
	
	/**
	 * Save a promotion
	 * @param promotion
	 */
	public void savePromotion(Promotion promotion) {
		getPromotionDAO().save(promotion);
	}
	
	/**
	 * Find promotion by id
	 * @param id
	 * @return
	 */
	public Promotion getPromotion(String id) {
		return getPromotionDAO().get(new ObjectId(id));
	}
	
	/**
	 * Delete promotion from database
	 * @param promotion
	 */
	public void deletePromotion(Promotion promotion) {
		getPromotionDAO().delete(promotion);
	}
	
	/**
	 * Delete promotion by id
	 * @param id
	 */
	public void deletePromotion(String id) {
		com.google.code.morphia.dao.BasicDAO<Promotion, ObjectId> dao = getPromotionDAO();
		Query<Promotion> query = dao.createQuery();
		query.field("id").equal(new ObjectId(id));
		dao.deleteByQuery(query);
	}
	
	/**
	 * Process the payment and create order, use for direct call from store
	 * @param user
	 * @param cart
	 * @param paymenttype
	 * @throws Exception
	 */
	public PaymentResult processPayment(User user, Cart cart, int paymenttype, PhoneCardInfo cardinfo) {
		if(log.isInfoEnabled())
			log.info(String.format("Process payment, user: %s - %s, card: %s, paymenttype: %s, phonecard: %s",
					String.valueOf(user.getUsername()), String.valueOf(user.getId()), cart == null ? "" : cart.toString(), paymenttype, cardinfo == null ? "" : cardinfo.toString()));
		
		FlashcardSetService fsrv = Repository.getInstance().getFlashcardSetService();
		UserService usrv = Repository.getInstance().getUserService();
		BasicDAO<Orderdetail> odao = getOrderDetailDAO();
		Orderdetail order;
		List<FlashcardSet> lsfcs = new ArrayList<FlashcardSet>();
		
		switch(paymenttype) {
		case Orderdetail.TYPE_COIN:
			if(user.getYellowCoin() < cart.getCointotal()) {
				log.warn(String.format("User %s not enough coin", user.getId()));
				return new PaymentResult("Coin not enought");
			}
			
			order = new Orderdetail(user, cart.getPricetotal(), cart.getCointotal(), Orderdetail.TYPE_COIN);
			order.setCode(generateOrderCode(user));
			order.setStatus(Orderdetail.STATUS_COMPLETE);
			order.setUserip(Executions.getCurrent().getRemoteAddr());
			
			for(CartItem item : cart.getItems()) {
				FlashcardSet fcs = fsrv.findByID(item.getProductid());
				if(fcs == null) {
					log.error("Unknow flashcardset: " + item.getProductname());
					return new PaymentResult("Unknow flashcardset " + item.getProductname());
				}
				
				if(fcs.getBlankflashcardset() != 0) {//this is a blank flashcardset
					user.setCurrentblankflashcard(user.getCurrentblankflashcard() + fcs.getTotalflashcards());
					if(log.isInfoEnabled())
						log.info(String.format("Buy blank flashcardset, %s cards, user have: %s cards", fcs.getTotalflashcards(), user.getCurrentblankflashcard()));
					
					//add this to history
					Orderproduct p = new Orderproduct();
					p.setCoin(item.getProductcoin());
					p.setPrice(item.getProductprice());
					p.setProductname(item.getProductname());
					p.setFlashcardset(fcs);
					p.setOrderdetail(order);
					order.getOrderproducts().add(p);
				}
				else {
					FlashcardSet nfcs = fcs.clone();
					nfcs.setAuthor(user);
					nfcs.setAdmin(FlashcardSet.ADMIN_BUY);
					nfcs.setPaymenttype(Orderdetail.TYPE_COIN);
					
					Orderproduct p = new Orderproduct();
					p.setCoin(item.getProductcoin());
					p.setPrice(item.getProductprice());
					p.setProductname(item.getProductname());
					p.setFlashcardset(fcs);
					p.setTargetset(nfcs);
					p.setOrderdetail(order);
					order.getOrderproducts().add(p);
					fsrv.save(nfcs);
				}
				
				applyPromotions(user, getAllValidPromotions(Promotion.TARGETTYPE_FLASHCARDSET, Promotion.CONDITIONTYPE_BUYITEM, fcs.getId()), true);
				lsfcs.add(fcs);
			}
			
			odao.save(order);
			user.setYellowCoin(user.getYellowCoin() - cart.getCointotal());
			usrv.save(user);
			if(log.isInfoEnabled())
				log.info(String.format("Created order: %s, user remains yellow coin: %s", String.valueOf(order), user.getYellowCoin()));
			
			//Add activity
			for(FlashcardSet item : lsfcs) {
				usrv.addActivityBuyItem(user, item);
			}
			return new PaymentResult(true, order, "");
		case Orderdetail.TYPE_NGANLUONG:
			//create the order, this will need one more step
			order = new Orderdetail(user, cart.getPricetotal(), cart.getCointotal(), Orderdetail.TYPE_NGANLUONG);
			order.setCode(generateOrderCode(user));
			order.setStatus(Orderdetail.STATUS_PAYMENT);
			order.setUserip(Executions.getCurrent().getRemoteAddr());
			
			for(CartItem item : cart.getItems()) {
				FlashcardSet fcs = fsrv.findByID(item.getProductid());
				if(fcs == null) {
					log.error("Unknow flashcardset: " + item.getProductname());
					return new PaymentResult("Unknow flashcardset " + item.getProductname());
				}
				
				Orderproduct p = new Orderproduct();
				p.setCoin(item.getProductcoin());
				p.setPrice(item.getProductprice());
				p.setProductname(item.getProductname());
				p.setFlashcardset(fcs);
				p.setOrderdetail(order);
				order.getOrderproducts().add(p);
			}
			
			odao.save(order);
			if(log.isInfoEnabled())
				log.info(String.format("Created order: %s, wait for payment from nganluong", String.valueOf(order)));
			
			return new PaymentResult(true, order, "");
			
		case Orderdetail.TYPE_CARD:
			if(cardinfo == null)
                return new PaymentResult("Không có thông tin card");
			
			//create order first
			order = new Orderdetail(user, cart.getPricetotal(), cart.getCointotal(), Orderdetail.TYPE_CARD);
			order.setCode(generateOrderCode(user));
			order.setStatus(Orderdetail.STATUS_PAYMENT);
			order.setUserip(Utility.getIp());
			
			for(CartItem item : cart.getItems()) {
				FlashcardSet fcs = fsrv.findByID(item.getProductid());
				if(fcs == null)
                    return new PaymentResult(String.format("Bộ từ %s không tồn tại", item.getProductname()));
				
				Orderproduct p = new Orderproduct();
				p.setCoin(item.getProductcoin());
				p.setPrice(item.getProductprice());
				p.setProductname(item.getProductname());
				p.setFlashcardset(fcs);
				p.setOrderdetail(order);
				order.getOrderproducts().add(p);
			}
			
			odao.save(order);
			
			//process the phonecard
			PaymentService ps = Repository.getInstance().getPaymentService();
			PhoneCardResponse rs = ps.processPhoneCard(cardinfo.getCard_pin(), cardinfo.getCard_seri(), cardinfo.getType_card(), order.getCode(), user.getEmail());
			
			order.addPaymentHistory("Process card: " + rs.toString());
			
			if(rs.isSuccess()) {//process phone card success
				int addedCoin = priceToCoinWithBonus(rs.getCard_amount());
				int avaiCoin = user.getYellowCoin();
				int neededCoin = order.getCointotal();
				
				if(log.isInfoEnabled())
					log.info(String.format("Price: %s, coin: %s", rs.getCard_amount(), addedCoin));
				
				if(neededCoin > addedCoin + avaiCoin) {//not enough coin
					order.setStatus(Orderdetail.STATUS_PAYMENTERROR);
					odao.save(order);
					user.setYellowCoin(addedCoin + avaiCoin);
					usrv.save(user);
                    return new PaymentResult(false, order, String.format("Bạn được %s/%s xu, xin nạp thêm", addedCoin + avaiCoin,
                            neededCoin));
				}
				else {
					order.setStatus(Orderdetail.STATUS_COMPLETE);
					Set<Long> pid = getPurchasedProductsId(user);
					
					//create card
					for(Orderproduct item : order.getOrderproducts()) {
						FlashcardSet fcs = fsrv.findByID(item.getFlashcardset().getId());
						if(fcs != null) {
							if(pid.contains(fcs.getId())) {
								order.addPaymentHistory(String.format("User %s already bought flashcardset %s", user.getId(), fcs.getId()));
							}
							else {
								FlashcardSet nfcs = fcs.clone();
								nfcs.setAdmin(FlashcardSet.ADMIN_BUY);
								nfcs.setAuthor(user);
								nfcs.setPaymenttype(Orderdetail.TYPE_CARD);
								item.setTargetset(nfcs);
								fsrv.save(nfcs);
								//add promotions
								applyPromotions(user, getAllValidPromotions(Promotion.TARGETTYPE_FLASHCARDSET, Promotion.CONDITIONTYPE_BUYITEM, fcs.getId()), true);
								//add activity
								usrv.addActivityBuyItem(user, item.getFlashcardset());
							}
						}
						else {
							log.error(String.format("Flashcardset %s in order %s not exists", item.getFlashcardset().getId(), order.getId()));
						}
					}
					
					odao.save(order);
					user.setYellowCoin(addedCoin + avaiCoin - neededCoin);
					usrv.save(user);
					
					return new PaymentResult(true, order, "");
				}
			}
			else {
				odao.save(order);
                return new PaymentResult(false, order, "Lỗi: " + rs.getMessage());
			}
		}
		
		return new PaymentResult("Unknow paymenttype to process");
	}
	
	/**
	 * Update order status with new message
	 * @param order
	 * @param newstatus
	 * @param message
	 * @return
	 */
	public PaymentResult updatePayment(Orderdetail order, int newstatus, String message, String transactionid) {
		if(order == null)
			return new PaymentResult("Unknow order");
		
		BasicDAO<Orderdetail> osrv = getOrderDetailDAO();
		FlashcardSetService fsrv = Repository.getInstance().getFlashcardSetService();
		UserService usrv = Repository.getInstance().getUserService();
		switch(newstatus) {
		case Orderdetail.STATUS_COMPLETE:
			order.setStatus(Orderdetail.STATUS_COMPLETE);
			order.setTransactionid(transactionid);
			//add the flashcardset
			for(Orderproduct item : order.getOrderproducts()) {
				if(item.getFlashcardset().getBlankflashcardset() == 1) {
					User author = order.getUser();
					author.setCurrentblankflashcard(author.getCurrentblankflashcard() + item.getFlashcardset().getTotalflashcards());
					usrv.save(author);
				}
				else {
					FlashcardSet nfcs = item.getFlashcardset().clone();
					nfcs.setAuthor(order.getUser());
					nfcs.setAdmin(FlashcardSet.ADMIN_BUY);
					nfcs.setPaymenttype(Orderdetail.TYPE_NGANLUONG);
					item.setTargetset(nfcs);
					fsrv.save(nfcs);
				}
				applyPromotions(order.getUser(), getAllValidPromotions(Promotion.TARGETTYPE_FLASHCARDSET, Promotion.CONDITIONTYPE_BUYITEM, item.getFlashcardset().getId()), true);
			}
			osrv.save(order);
			
			//Add activity
			for(Orderproduct item : order.getOrderproducts()) {
				usrv.addActivityBuyItem(order.getUser(), item.getFlashcardset());
			}
			return new PaymentResult(true, order, "");
		case Orderdetail.STATUS_PAYMENTERROR:
			order.setStatus(Orderdetail.STATUS_PAYMENTERROR);
			order.addPaymentHistory(message);
			osrv.save(order);
			return new PaymentResult(false, order, message);
		case Orderdetail.STATUS_CANCEL:
			order.setStatus(Orderdetail.STATUS_CANCEL);
			order.addPaymentHistory("Payment cancel from user");
			osrv.save(order);
			return new PaymentResult(false, order, message);
		}
		return new PaymentResult("Unknow status");
	}
	
	/**
	 * Process the payment by phonecard and update the order status
	 * @param order
	 * @param cardinfo
	 * @return
	 */
	public PaymentResult updatePayment(Orderdetail order, PhoneCardInfo cardinfo) {
		UserService usrv = Repository.getInstance().getUserService();
		BasicDAO<Orderdetail> odao = getOrderDetailDAO();
		FlashcardSetService fsrv = Repository.getInstance().getFlashcardSetService();
		User user = order.getUser();
		
		if(cardinfo == null)
            return new PaymentResult(false, order, "Không có thông tin card");
		
		PaymentService ps = Repository.getInstance().getPaymentService();
		PhoneCardResponse rs = ps.processPhoneCard(cardinfo.getCard_pin(), cardinfo.getCard_seri(), cardinfo.getType_card(), order.getCode(), user.getEmail());
		
		order.addPaymentHistory("Process card: " + rs.toString());
		
		if(rs.isSuccess()) {
			int addedCoin = priceToCoinWithBonus(rs.getCard_amount());
			int avaiCoin = user.getYellowCoin();
			int neededCoin = order.getCointotal();
			
			if(neededCoin > addedCoin + avaiCoin) {//not enough coin
				order.setStatus(Orderdetail.STATUS_PAYMENTERROR);
				odao.save(order);
				user.setYellowCoin(addedCoin + avaiCoin);
				usrv.save(user);
                return new PaymentResult(false, order, String.format("Bạn được %s/%s xu, xin nạp thêm", addedCoin + avaiCoin, neededCoin));
			}
			else {
				order.setStatus(Orderdetail.STATUS_COMPLETE);
				Set<Long> pid = getPurchasedProductsId(user);
				
				//create card
				for(Orderproduct item : order.getOrderproducts()) {
					FlashcardSet fcs = item.getFlashcardset();
					if(pid.contains(fcs.getId())) {
						order.addPaymentHistory(String.format("User %s already bought flashcardset %s", user.getId(), fcs.getId()));
					}
					else {
						FlashcardSet nfcs = fcs.clone();
						nfcs.setAdmin(FlashcardSet.ADMIN_BUY);
						nfcs.setAuthor(user);
						nfcs.setPaymenttype(Orderdetail.TYPE_CARD);
						item.setTargetset(nfcs);
						fsrv.save(nfcs);
						//add promotions
						applyPromotions(user, getAllValidPromotions(Promotion.TARGETTYPE_FLASHCARDSET, Promotion.CONDITIONTYPE_BUYITEM, fcs.getId()), true);
						//add activity
						usrv.addActivityBuyItem(user, item.getFlashcardset());
					}
				}
				
				odao.save(order);
				user.setYellowCoin(addedCoin + avaiCoin - neededCoin);
				usrv.save(user);
				
				return new PaymentResult(true, order, "");
			}
		}
		else {
			odao.save(order);
            return new PaymentResult(false, order, "Lỗi: " + rs.getMessage());
		}
	}
	
	/**
	 * Generate unique code for order
	 * @return
	 */
	public String generateOrderCode(User user) {
		return String.format("%s-%s", user.getId(), new Date().getTime());
	}
	
	/**
	 * Get product code with history from database by user id
	 * @param user id
	 */
	public List<ProductCode> findProductCodeByUser(Long userId) {
		//TODO optimize this
		Query<ProductCode> query = getProductCodeDAO().createQuery();
		List<ProductCode> lst = query.asList();
		if(lst.size() > 0) {
			for(int i = 0; i < lst.size(); i++) {
				ProductCode pc = lst.get(i);
				List<ProductCodeHistory> pchl = pc.getUseHistory();
				if(pchl.size() > 0) {
					for(int j = 0; j < pchl.size(); j++) {
						ProductCodeHistory pch = pchl.get(j);
						if(pch.getUserId() != userId) {
							pchl.remove(pch);
						}
					}
					if(pchl.size() == 0) {
						lst.remove(pc);
					}
				}
			}
		}
		return lst;
	}
	
	/**
	 * Find a product code by code
	 * @param code
	 * @return
	 */
	public ProductCode getProductCode(String code) {
		com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId> dao = getProductCodeDAO();
		Query<ProductCode> query = dao.createQuery();
		query.field("code").equal(code.toUpperCase());
		return query.get();
	}
	
	/**
	 * Get all products code
	 * @param page
	 * @param pagesize
	 * @return
	 */
	public List<ProductCode> getProductCodes(int page, int pagesize) {
		com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId> dao = getProductCodeDAO();
		Query<ProductCode> query = dao.createQuery();
		query.limit(pagesize);
		query.offset(page * pagesize);
		query.order("-created");
		return query.asList();
	}
	
	/**
	 * Count the total number of product code
	 * @return
	 */
	public long countProductCodes() {
		com.google.code.morphia.dao.BasicDAO<ProductCode, ObjectId> dao = getProductCodeDAO();
		Query<ProductCode> query = dao.createQuery();
		return query.countAll();
	}
	
	/**
	 * User add code to add new product
	 * @param user
	 * @param code
	 * @return ActionResult
	 */
	public ActionResult userAddCode(User user, String code) {
		if(log.isInfoEnabled())
			log.info(String.format("User: %s wants to add product code: %s", user == null ? "Unknow" : user.getUsername(), code));
		
		ActionResult rs = new ActionResult();
        code = code.trim();
		
		if(user == null || user.getId() == null) {
			rs.addMessage("Unknow user");
			log.error("Unknow user");
		} else {
			//check to see if the code is valid
			ProductCode productcode = getProductCode(code);
			if(productcode == null) {
				rs.addMessage("Unknow code");
				log.error("Unknow code");
			}
			else if(productcode.getStatus() == ProductCode.STATUS_EXPIRED) {
				rs.addMessage("Expired code");
				log.error("Expired code");
			}
			else if(productcode.getStatus() == ProductCode.STATUS_USED) {
				rs.addMessage("Used code");
				log.error("Used code");
			}
			else if(productcode.getExpireOn() != null && productcode.getExpireOn().before(new Date())) {
				rs.addMessage("Expired code");
				productcode.setStatus(ProductCode.STATUS_EXPIRED);
				saveProductCode(productcode);
				log.error("Expired code 1");
			}
			else {
				ProductCodeHistory his = new ProductCodeHistory(); //store the history using this code
				his.setUsedIp(Utility.getIp());
				his.setUserId(user.getId());
				his.setUserName(user.getUsername());
				
				if(productcode.getProducts().size() > 0) {
					FlashcardSetService srv = Repository.getInstance().getFlashcardSetService();
				
					Set<Long> pid = getPurchasedProductsId(user); //already purchased products
					for(Entry<Long, String> entry : productcode.getProducts().entrySet()) {
						//entry: <productid, productname>
						Long fcsid = entry.getKey();
						FlashcardSet fcs = srv.findByID(fcsid);
						if(fcs == null) {
							rs.addMessage(String.format("Unknow fcs id: %s, name: %s", fcsid, entry.getValue()));
							log.error(rs.getMessage());
							his.addProduct(fcsid, (long) ProductCodeHistory.STATUS_UNKNOWSET);
						}
						else if(fcs.getAdmin() != FlashcardSet.ADMIN_YES) {
							rs.addMessage(String.format("Fcs id: %s, name: %s is not of blueup", fcsid, entry.getValue()));
							log.error(rs.getMessage());
							his.addProduct(fcsid, (long) ProductCodeHistory.STATUS_NOTBLUEUPSET);
						}
						else {
							//check to see if user already has this flashcardset
							if(pid.contains(fcs.getId())) {
								rs.addMessage(String.format("Fcs id: %s, name: %s already in your dashboard", fcsid, entry.getValue()));
								his.addProduct(fcsid, (long) ProductCodeHistory.STATUS_DUPPLICATEDSET);
								rs.addObject(fcs);
								
								if(log.isInfoEnabled())
									log.info(String.format("Fcs id: %s name: %s already in dashboard", fcsid, entry.getValue()));
							}
							else {
								FlashcardSet nfcs = fcs.clone();
								nfcs.setAdmin(FlashcardSet.ADMIN_BUY);
								nfcs.setAuthor(user);
								nfcs.setPaymenttype(Orderdetail.TYPE_CODE);
								srv.save(nfcs);
								applyPromotions(user, getAllValidPromotions(Promotion.TARGETTYPE_FLASHCARDSET, Promotion.CONDITIONTYPE_BUYITEM, fcs.getId()), false);
	
								his.addProduct(fcsid, nfcs.getId());
	
								rs.addObject(nfcs);
	
								if(log.isInfoEnabled())
									log.info(String.format("Product code: %s, clone: %s to: %s", code, fcs.getId(), nfcs.getId()));
							}
						}
					}
				}
				
				productcode.addUseHistory(his);
				saveProductCode(productcode);
				
				rs.setError(false);
				
				//apply promotion
				applyPromotions(user, Promotion.TARGETTYPE_CODE, Promotion.CONDITIONTYPE_ACTIVECODE, productcode.getCode(), false);
			}
		}
		
		return rs;
	}
	
	/**
	 * Find a product category by id
	 * @param id
	 * @return
	 */
	public Productcategory getProductCategory(Long id) {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		return dao.findByID(id);
	}
	
	/**
	 * Get list of product categories by name
	 * @return
	 */
	public Productcategory getProductCategoryByName(String name) {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		Criteria crit = dao.createCriteria();
		crit.add(Restrictions.isNull("parent"));
		crit.add(Restrictions.eq("name", name));
		crit.addOrder(Order.asc("name"));
		return (Productcategory)crit.uniqueResult();
	}
	
	/**
	 * Get list of product categories by slug
	 * @return
	 */
	public Productcategory getProductCategoryBySlug(String slug) {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		Criteria crit = dao.createCriteria();
		crit.add(Restrictions.isNull("parent"));
		crit.add(Restrictions.eq("slug", slug));
		crit.addOrder(Order.asc("name"));
		return (Productcategory)crit.uniqueResult();
	}
	
	/**
	 * Save a product category to database
	 * @param category
	 */
	public void saveProductCategory(Productcategory category) {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		category.setSlug(TextUtil.removeAccent(category.getName()).toLowerCase());
		dao.save(category);
	}
	
	/**
	 * Delete a category from database
	 * @param category
	 */
	public void deleteProductCategory(Productcategory category) {
		BasicDAO<Productcategory> dao = getProductCategoryDAO();
		dao.delete(category);
	}
	
	/**
	 * Save history to database
	 * @param history
	 */
	public void saveAddCoinHistory(AddCoinHistory history) {
		getAddCoinDAO().save(history);
	}
	
	/**
	 * Delete addcoin history
	 * @param history
	 */
	public void deleteAddCoinHistory(AddCoinHistory history) {
		getAddCoinDAO().delete(history);
	}
	
	/**
	 * Find a history by id
	 * @param id
	 * @return
	 */
	public AddCoinHistory getAddCoinHistory(String id) {
		return getAddCoinDAO().get(new ObjectId(id));
	}
	
	/**
	 * User want to add new coin to account, create an entry in database to store it
	 * @param user
	 * @param amount
	 * @param paymenttype
	 * @return
	 */
	public AddCoinHistory newAddCoinHistory(User user, int amount, int paymenttype) {
		if(user == null || amount == 0)
			return null;
		
		com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId> dao = getAddCoinDAO();
		AddCoinHistory his = new AddCoinHistory();
		his.setAmount(amount);
		his.setCoin(priceToCoinWithBonus(amount));
		his.setPaymenttype(paymenttype);
		his.setUserid(user.getId());
		dao.save(his);
		
		if(log.isInfoEnabled())
			log.info(String.format("User %s wants to buy %s yellow coins by method %s", user.getUsername(), amount, paymenttype));
		
		return his;
	}
	
	/**
	 * Update the status for addcoinhistory
	 * @param history
	 * @param newstatus
	 * @param message
	 * @return
	 */
	public PaymentResult updatePayment(AddCoinHistory history, int newstatus, String message) {
		com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId> dao = getAddCoinDAO();
		UserService usrv = Repository.getInstance().getUserService();
		User user;
		switch(newstatus) {
		case AddCoinHistory.STATUS_COMPLETE:
			user = usrv.findByID(history.getUserid());
			if(user == null)
                return new PaymentResult(false, history, "Không rõ tài khoản người dùng");
			history.setStatus(AddCoinHistory.STATUS_COMPLETE);
			history.addHistory(message);
			user.setYellowCoin(user.getYellowCoin() + history.getCoin());
			dao.save(history);
			usrv.save(user);
			return new PaymentResult(true, history, "");
		case AddCoinHistory.STATUS_CANCEL:
			user = usrv.findByID(history.getUserid());
			if(user == null)
                return new PaymentResult(false, history, "Không rõ tài khoản người dùng");
			history.setStatus(AddCoinHistory.STATUS_CANCEL);
			history.addHistory(message);
			dao.save(history);
			return new PaymentResult(true, history, "");
		}
		return null;
	}
	
	/**
	 * Process the payment by phonecard and update the add coin status
	 * @param history
	 * @param cardinfo
	 * @return
	 */
	public PaymentResult updatePayment(AddCoinHistory history, PhoneCardInfo cardinfo) {
		
		if(log.isInfoEnabled())
			log.info(String.format("Process history %s with card %s", history.getId(), cardinfo.toString()));
		
		com.google.code.morphia.dao.BasicDAO<AddCoinHistory, ObjectId> dao = getAddCoinDAO();
		UserService usrv = Repository.getInstance().getUserService();
		User user = usrv.findByID(history.getUserid());
		
		if(user == null) {
			log.error(String.format("Unknow user %s", history.getUserid()));
            return new PaymentResult(false, history, "Tài khoản người dùng không tồn tại");
		}
		
		if(cardinfo == null) {
			log.error("Unknow phone card");
            return new PaymentResult(false, history, "Chưa nhập thông tin card");
		}
		
		PaymentService ps = Repository.getInstance().getPaymentService();
		PhoneCardResponse rs = ps.processPhoneCard(cardinfo.getCard_pin(), cardinfo.getCard_seri(), cardinfo.getType_card(), history.getId().toString(), user.getEmail());
		
		history.addHistory("Card: " + rs.toString());
		history.addCardResponse(rs);
		
		if(rs.isSuccess()) {
			if(log.isInfoEnabled())
				log.info("Process success");
			
			history.setStatus(AddCoinHistory.STATUS_COMPLETE);
			history.addHistory(rs.getMessage());
			int coin = priceToCoinWithBonus(rs.getCard_amount());
			
			history.setCoin(coin);
			history.setAmount(rs.getCard_amount());
			dao.save(history);
			
			if(log.isInfoEnabled())
				log.info(String.format("Price: %s, coin: %s", rs.getCard_amount(), coin));
			
			user.setYellowCoin(user.getYellowCoin() + coin);
			usrv.save(user);
			return new PaymentResult(true, history, "");
		}
		else {
			if(log.isInfoEnabled())
				log.info("Process fail " + rs.getMessage());
			
			history.setStatus(AddCoinHistory.STATUS_ERROR);
			history.addHistory(rs.getMessage());
			dao.save(history);
            return new PaymentResult(false, history, "Lỗi : " + rs.getMessage());
		}
	}
	
	/** PARTNER SECTION **/
	public List<Partner> getPartners() {
		com.google.code.morphia.dao.BasicDAO<Partner, ObjectId> dao = getPartnerDAO();
		Query<Partner> query = dao.createQuery();
		query.order("name");
		return query.asList();
	}
	
	/**
	 * Save partner information
	 * @param partner
	 */
	public void savePartner(Partner partner) {
		getPartnerDAO().save(partner);
	}
}